Mapping II

GESIS Workshop: Introduction to Geospatial Techniques for Social Scientists in R

Stefan Jünger & Dennis Abel

2025-04-10

Now

Day Time Title
April 09 10:00-11:30 Introduction
April 09 11:30-11:45 Coffee Break
April 09 11:45-13:00 Data Formats
April 09 13:00-14:00 Lunch Break
April 09 14:00-15:30 Mapping I
April 09 15:30-15:45 Coffee Break
April 09 15:45-17:00 Spatial Wrangling
April 10 09:00-10:30 Mapping II
April 10 10:30-10:45 Coffee Break
April 10 10:45-12:00 Applied Spatial Linking
April 10 12:00-13:00 Lunch Break
April 10 13:00-14:30 Spatial Autocorrelation
April 10 14:30-14:45 Coffee Break
April 10 14:45-16:00 Spatial Econometrics & Outlook

What is ggplot2?

ggplot2 is well-known for creating plots. Thanks to our sf and terra, we can exploit all amazing ggplot2 functions.

In general, on ggplot2:

  • Well-suited for multi-dimensional data
  • Expects data (frames) as input
  • Components of the plot are added as layers
plot_call +
  layer_1 +
  layer_2 +
  ... +
  layer_n

From tmap to ggplot2

Reminder: We played around with tmap yesterday, and the results were already pretty nice. ggplot2 allows us to customize our maps even more, draw on previous knowledge of the package, increase the possibilities to combine maps and plots, and more.

The good thing is that the inner logic of tmap and ggplot2 is the same and is based on the grammar of graphics.

If you are new to ggplot2, you might want to check out:

Components of a Plot

According to Wickham (2010, p. 81), a layered plot consists of the following components:

  • Data and aesthetic mappings,
  • Geometric objects,
  • Scales,
  • (and facet specification)
plot_call +
  data +
  aesthetics +
  geometries +
  scales +
  facets

Let us start building some maps!

First: Get the data!

# load district shapefile
german_districts <- sf::read_sf("./data/VG250_KRS.shp")

# load district attributes
attributes_districts <- 
  readr::read_csv2("./data/attributes_districts.csv") |> 
  dplyr::mutate(ecar_share = as.numeric(ecar_share))

# join data
german_districts_enhanced <- 
  german_districts |>  
  dplyr::left_join(attributes_districts, by = "AGS")

# load states shapefile
german_states <- sf::read_sf("./data/VG250_LAN.shp")

Here’s a first basic map

# a simple first map 
ggplot() +
  geom_sf(data = german_districts_enhanced)

Making a plan

This map will be our canvas for the ongoing session. There are hundreds of options to change this map. We will cover at least some essential building blocks:

  • THE MAP: adding attributes, choosing from colors/palettes, adding layers
  • THE LEGEND: position, sizes, display
  • THE ENVIRONMENT: choosing from themes and build your own
  • THE META-INFORMATION: titles and sources
  • THE EXTRAS: scales and compass

If you are working on your maps, the ggplot2 cheatsheets will help you with an overview of scales, themes, labels, facets, and more.

The map layer: a basis

# easy fill with color
ggplot() +
  geom_sf(
    data = german_districts_enhanced, 
    fill = "purple", 
    color = "blue"
  )

Add the aesthetics

We’ll concentrate on mapping the e-car share at the district level.

# map aethetics
ggplot() +
  geom_sf(
    data = german_districts_enhanced, 
    # add the attribute we want to map
    aes(fill = ecar_share)
  ) + 
  # choose a continuous palette 
  scale_fill_continuous() 

The map layer

Are you having trouble choosing the right color? Some excellent tutorials exist, f.e, by Michael Toth.

# change color palette
ggplot() +
  geom_sf(
    data = german_districts_enhanced, 
    aes(fill = ecar_share)
  ) + 
  # readable with color vision deficiencies
  scale_fill_viridis_c(option = "plasma") 

The map layer

You are changing your map step by step.

ggplot() +
  geom_sf(
    data = german_districts_enhanced, 
    aes(fill = ecar_share), 
    # make the borders disappear
    color = NA
  ) +
  scale_fill_viridis_c(
    option = "plasma",
    # change scale direction
    direction = -1
  )  

Add another layer

# the shapefile includes polygons of oceans and lakes
# easy fix on the fly when you know your data
german_states <-
  german_states |>  
  dplyr::filter(GF == 4)

# add layer with German states
ggplot() +
  geom_sf(
    data = german_districts_enhanced, 
    aes(fill = ecar_share), 
    color = NA
  ) + 
  scale_fill_viridis_c(
    option = "plasma", 
    direction = -1
  ) +
  # add another layer
  geom_sf(
    data = german_states, 
    # filling transparent
    fill = "transparent",
    # color of borders
    color = "black", 
    # size of borders
    size = 1
  )  

Dealing with the Legend

You can handle everything concerning the legend (labels, titles, width…) within the scale argument. The only thing you cannot change here is the position in relation to the map.

ggplot() +
  geom_sf(
    data = german_districts_enhanced, 
    aes(fill = ecar_share), 
    color = NA
  ) + 
  scale_fill_viridis_c(
    option = "plasma",
    direction = -1,
    # add a legend title
    name = "E-Car Share",
    # adjust legend
    guide = guide_legend(
      # turn it horizontal
      direction= "horizontal",
      # put the labels
      # under the legend bar
      label.position = "bottom"
    )
  ) + 
  geom_sf(
    data = german_states, 
    fill = "transparent", 
    color = "black"
  ) 

# check the help file for more options ?guide_legend

Save and reuse

Maps produced with ggplot2 are standard objects like any other object in R (they are lists). We can assign them to reuse, plot later, and add map layers.

Furthermore, you can save them just as any ggplot2 graph. The ggsave() function automatically detects the file format. You can also define the height, width, and dpi, which is particularly useful to produce high-class graphics for publications.

Save and reuse

# assign to object
ecar_map <- 
  ggplot() +
  geom_sf(
    data = german_districts_enhanced, 
    aes(fill = ecar_share), 
    color = NA
  ) + 
  scale_fill_viridis_c(
    option = "plasma",
    direction = -1,
    name = "E-Car Share",
    guide = guide_legend(
      direction= "horizontal",
      label.position = "bottom"
    )
  ) + 
  geom_sf(
    data = german_states, 
    fill = "transparent", 
    color = "black"
  ) 

# save as png-file
# ggsave("ecar_map.png", ecar_map, dpi = 300)

Get rid of everything?!

The theme controls all ‘non-data’ displays. If you want to get rid of the default ggplot2 theme, you can do so. Instead of removing everything, you should try out the built-in themes.

# use the object ecar_map as base layer
ecar_map +
  # empty your theme
  theme_void() 


# ... or add another
# theme_bw()
# theme_gray()
# theme_light()

# check all themes here
# ?theme

Build your own theme

# building a theme
ecar_map +
  theme_void() + 
  # bold text elements
  theme(
    title = element_text(face = "bold"), 
    # move legend to bottom of map
    legend.position = "bottom", 
    # change background color
    panel.background = 
      element_rect(fill = "lightgrey")
  )

Adding labs

There is one necessary step to do. You should always make sure to include and cite your data sources. Especially in graphs and maps, you can use a short version to include them directly in the description.

ecar_map +
  # add title
  labs(
    title = 
      "E-Car Share in Germany",   
    # add sub-title
    subtitle = 
      "Where are the regional differences across German districts?",   
    # add source
    caption =  
      "© Bundesnetzagentur"
  ) 

Exercise 5_1: Advanced Maps

Exercise

To be continued…

Our code has already grown. Without going into too much detail, the following slides showcase some more changes you can make to your maps

A map is never finished until you decide not to work on it anymore.

Creating a city layer for city labels

# create a german city layer by choosing the five districts
# with the highest pop density

districts_centroids <-
  german_districts_enhanced |>  
  # calculate pop_dens
  dplyr::mutate(
    pop_dens = population / sf::st_area(german_districts_enhanced)
    ) |> 
  # filter top 5 observation with highs pop_dens
  dplyr::top_n(5, pop_dens) |>  
  # take the centroid of each polygon and turn to
  # polygon file into a vector
  sf::st_centroid() 

city_coordinates <-
  districts_centroids |> 
  sf::st_coordinates() |> 
  as.data.frame() 

german_cities <- 
  dplyr::bind_cols(districts_centroids, city_coordinates) |> 
  # add some city names as labels
  dplyr::bind_cols(
    data.frame(
      names = c("City 1", "City 2", "City 3", "City 4", "City 5")
    )
  )

german_cities |> 
  dplyr::select(pop_dens, X, Y)
Simple feature collection with 5 features and 3 fields
Geometry type: POINT
Dimension:     XY
Bounding box:  xmin: 432953.5 ymin: 5283785 xmax: 670634.3 ymax: 5998091
Projected CRS: ETRS89 / UTM zone 32N
# A tibble: 5 × 4
  pop_dens       X        Y           geometry
   [1/m^2]   <dbl>    <dbl>        <POINT [m]>
1    2.03  501787. 5964329. (501786.6 5964329)
2    0.786 432954. 5947975. (432953.5 5947975)
3    1.25  469708. 5935810. (469708.3 5935810)
4    0.933 504505. 5283785. (504505.2 5283785)
5    2.07  670634. 5998091. (670634.3 5998091)

Add City Labels

Using geom_label to get a text box with a character string associated with an X- and Y-coordinate.

ecar_map +
  # add the label
  geom_label(
    data = german_cities, 
    # don't need sf object but columns 
    # with x- and  y-coordinate
    aes(
      x = X, y = Y, 
      # column holding the character
      # vector with strings
      label = names
    ),
    # size of labels
    size = 3,
    # transparency
    alpha = .8
  )

ggplot2 and raster data

You can also use ggplot2 to create maps with raster data. There are several ways to do so. The easiest way is using the tidyterra package.

cologne_immigrants <- 
  terra::rast("../../data/immigrants_cologne.tif")

ggplot() +
  tidyterra::geom_spatraster(
    data = cologne_immigrants, 
    aes(fill = immigrants_cologne)
  ) +
  # set na values transparent
  scale_fill_continuous(
    na.value = "transparent"
  ) +
  # remove theme
  theme_void()

Where ggplot2 cannot help anymore

In some specific circumstances, we might realize that ggplot2 is super powerful but not originally designed to build maps. Typical features of maps are not in the package, like a compass or scale bars.

This is where other packages might need to be installed. The good thing: Elements of the package ggspatial can be included as ggplot2 layer. Check out Github.

The extras

ggspatial allows you to add, f.e. a scale bar and a north arrow.

# add scalebar and north arrow
ecar_map +
  ggspatial::annotation_scale(
    location = "br"
  ) +
  ggspatial::annotation_north_arrow(
    location = "tr", 
    style = ggspatial::north_arrow_minimal()
  )

Exercise 5_2: The Perfect Map

Exercise